#What is this?

    #This code enables a Rasbberry Pi to act as a GPIB-enabled Instrument. To be clear, this is not a GPIB controller, rather
    #this allows you to build an arbitrary instrument using the Pi and then use a 'Commercial-Off-The-Shelf' GPIB controller to communicate with the Pi,
    #along with an arbitrary assortment of Commercial-Off-The-Shelf instruments operating on the same GPIB Bus.
    #It was written specifically for the KB2040. The KB2040 is an Adafruit implementation
    #of the RP2040, on a carrier with typical I-O hardware and headers. Other RP implementations would require a remapping of pins
    #and the neopixel indicator.

#Notes on  GPIB Documentation:

    #The source-acceptor handshake is well documented in the 1978 GPIB standard and later IEEE 488.2 standard.
    #The common *IDN? protocol is documented under  IEEE 488.2 part 2 section 10.14.
    #The IEEE 488.2 standard appears tacit or out-of-date on some of the details required for talker-listener handoff and bus-scans to
    #identify instruments. Section 17-6 of IEEE 488.2 is notable in that the pseudocode therein does not perform to expectations.

    #With the detail on many key protocols implied (rather than explicit) through the combination of state machines scattered 
    #throuough the documentation, much of the practical operation of the GPIB bus was inferred by
    #trial and error and use of a logic analyzer to observe controller-instrument exchanges.

    #To be compatible with this software/hardware build, the controller's bus timeout should be in the 3-5 ms region.

    #Code written by Matthew.Spidell@protonmail.com Jun-Sep 2025

#Below is the physical connection table for the GPIB Connector(Centronix-type 24-pin Sub-D) and KB2040 pins:

    #GPIB DIO1 to DIO7 (Centronix connector Pins 1-4 and 13-15) connect to KB2040 D0 to D6
    #GPIB DIO8 (Centronix connector pin 16) is unconnected
    #GPIB NDAC (Centronix connector pin 8) connects to KB2040 MOSI (prototype breakout board lists the HPIB name (DAC) vs the GPIB name (NDAC). This means that DAC is literal using True-False in this code. This getts confusing with everything other than RFD and DAC being asserted low)
    #GPIB NRFD (Centronix connector pin 7) connects to KB2040 MISO (prototype breakout board lists the HPIB name (DAC) vs the GPIB name (NDAC). This means that RFD is literal using True-False in this code. This getts confusing with everything other than RFD and DAC being asserted low)
    #GPIB DAV (Centronix connector pin 6) connects to KB2040 D7
    #GPIB ATN (Centronix connector pin 11) connects to KB2040 D8 (prototype breakout board lists the HPIB name (MRE) vs the GPIB name (ATN))
    #GPIB REN (Centronix connector pin 17) connects to KB2040 D9
    #GPIB EOI (Centronix connector pin 5) connects to KB2040 D10

#Breakout Board: https://oshpark.com/shared_projects/n6hMKXOe

#Description of specific Products or OEMs in this work does not imply endorsement or suitability.
###########################################################################################

import time
import board
import digitalio
import analogio
from board import * #not sure what this is but it's needed for analog in
import neopixel

#set the neopixel initially off
pixels = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixels.fill((0, 0, 0))

#Assign physical pins to Inputs & Outputs, assign drive and pulls
DAC= digitalio.DigitalInOut(board.MOSI)
DAC.direction = digitalio.Direction.OUTPUT
DAC.drive_mode= digitalio.DriveMode.OPEN_DRAIN
DAC.value=False #RFD and DAC assertion is literal. A DAC of 'False' is a 'data not accepted' state

RFD= digitalio.DigitalInOut(board.MISO)
RFD.direction = digitalio.Direction.OUTPUT
RFD.drive_mode= digitalio.DriveMode.OPEN_DRAIN
RFD.value=False #RFD and DAC assertion is literal. A RFD of 'False' is a 'not ready for data' state

DIO1 = digitalio.DigitalInOut(board.D0)
DIO2 = digitalio.DigitalInOut(board.D1)
DIO3 = digitalio.DigitalInOut(board.D2)
DIO4 = digitalio.DigitalInOut(board.D3)
DIO5 = digitalio.DigitalInOut(board.D4)
DIO6 = digitalio.DigitalInOut(board.D5)
DIO7 = digitalio.DigitalInOut(board.D6)

DIO_BUS = [DIO1, DIO2, DIO3, DIO4, DIO5, DIO6, DIO7]

for DIO in DIO_BUS:
    DIO.direction = digitalio.Direction.INPUT
    DIO.pull = digitalio.Pull.UP

#DAV, MRE, REN, EOI, (and other  control bus lines I'm not using) assert at 0V or 'False'. This gets confusing
#because 0 V for the microcontroller is 'False' while in the IEEE488.2 or GPIB standards 0 V is 'True' or 'Assert' for these lines.
#Avoid confusion by thinking of 'Assert' at 0 V, and avoid thinking of 'True' at 0 V. Likewise, 3.3 V is best thought of as 'not asserted'. 
#
#Now, consider RFD and DAC. The 1978 GPIB standard describes RFD and DAC lines which become NDAC and NRFD in IEEE488.2. 
#Therefore, IEEE488.2 documentation is a standard where 'Not Ready For Data' asserts at 0 V, which is a microcontroller 'False'.
#That double negative just got too confusing, even if it is consistent with assert at 0 V. I found it reasier to consider that 
#DAC and RFD are litteral when we strike the N or Not from the nomenclature for these lines.
#When we strike the 'Not' from these lines, a microcontroller 'True' of 3.3 V matches the statements 'Ready For Data' (RFD) and 'Data Accepted' (DAC)
DAV = digitalio.DigitalInOut(board.D7)
DAV.direction = digitalio.Direction.INPUT
DAV.pull = digitalio.Pull.UP
MRE = digitalio.DigitalInOut(board.D8)
MRE.direction = digitalio.Direction.INPUT
MRE.pull = digitalio.Pull.UP
REN = digitalio.DigitalInOut(board.D9)
REN.direction = digitalio.Direction.INPUT
REN.pull = digitalio.Pull.UP
EOI = digitalio.DigitalInOut(board.D10)  #
EOI.direction = digitalio.Direction.INPUT
EOI.pull = digitalio.Pull.UP

analog_in_1 = analogio.AnalogIn(A0) #configuring A0 as an analog input to demonstrate data acquisition

py_address_integer=4
#Pre-calculated talk/listen addresses could be used in lieu of runtime calcs for speed.
#py_listen_address=int(0b0100000)+py_address_integer
#py_talk_address=int(0b1000000)+py_address_integer

GPIB_Char=0 #preloading this variable
listen=False #preloading this variable
message_in_stack="" #preloading this variable
prev_message_in_stack="" #preloading this variable
blink_light_flag=False #preloading this variable
string_out="Nul" #preloading this variable
strobe_brightness=0 #preloading this variable
strobe_color="r" #preloading this variable
query_flag=False #preloading this variable
LED_Dir=True #preloading this variable
DAV_counts=0 #preloading this variable

print("Code as of 9/05 3:30p")
pixels.fill((20, 6, 0)) #yellow indicates initial-on, no controller detected


RFD.value=True #RFD and DAC assertion is literal. A RFD of 'true' is a ready-for-data state.

while True:
    #infinite loop serves as a 'shell' for the program.
    while DAV.value: #DAV asserts at a logic low, ~0 V or 'False'
        #This loop waits for the DAV line to assert (0V or 'False'). We need to wait here as the next block reads the data
        #byte from the controller. While in this loop, we can run some idle functions:
        #   1) Recognising when an inactionable message was sent (by assuming that if we are in the
        #      loop longer than normal (about 200-300 loops at the speed of the RP2040) that the controller has exausted
        #      its register and if that message was not acted on, it's probably a bad command.
        #      I've set the number of loops at 500, but a larger count may be appropriate.
        #   2) Strobe the LED to serve as a visual indicator regarding device status
        DAV_counts=DAV_counts+1
        if DAV_counts>500:
            #This case occures when the controller has stopped sending characters (i.e. DAV is not asserted)
            #Functionally, this is analogous to a timeout and in an modern OEM instrument would drive a BEEP and flag an error
            #Consider this for future development.
            if message_in_stack!="":
                print("probably controller fault or bad command")
                message_in_stack=""
            DAV_counts=0

        if REN.value==True:
            #Clear listen and clear message when REN is not asserted (asserted for REN is 0V or False)
            message_in_stack=""
            listen=False
            strobe_color="g"
            #DAC.value=True # This might cause the code to fail on older GPIB controllers.
            #RFD.value=True # This might cause the code to fail on older GPIB controllers.
        else:
            strobe_color="r"
            #DAC.value=False
            #RFD.value=True

        if MRE.value:
            #Strobe the green for 'local' and strobe the red led to indicate device is in 'remote'.
            #Ideally, this would be an unconditional clde block. But, because the NeoPixel write process
            #is slow, we need to be careful on how we handle the strobing in the case that the MRE
            #line is asserted (asserted for MRE is 0V or False) as the speed of the 'scan for listeners'
            #process is too fast for the RP2040, when burdened with the NeoPixel instructions.
            #The LED/Neopixel control might be better to run on the second core of the RP2040
            if LED_Dir:
                strobe_brightness=strobe_brightness+.02
                if strobe_brightness>6:
                    LED_Dir=False
            else:
                strobe_brightness=strobe_brightness-.02
                if strobe_brightness<1:
                    LED_Dir=True

            if strobe_color=="r":
                pixels.fill((strobe_brightness, 0, 0))
            elif strobe_color=="g":
                pixels.fill((0, strobe_brightness , 0))

    DAV_counts=0
    GPIB_Char=0

    #we arrive here if DAV asserts (goes low). Therefore, it's time to read the data byte and controller commands.
    is_bus_instruction=not MRE.value #MRE asserts low or 0V. Capture bus MRE and EOI commands concurrent with data
    is_endof_instruction=not EOI.value #EOI asserts low or 0V.
    is_remote=not REN.value

    if is_bus_instruction or listen:
    #When the data is not a bus instruction or we are not 'listening' there's no need to
    #interpret the DIO lines. Just get on with the handshake to avoid delaying other instruments.
        for bit_index, DIO in enumerate(DIO_BUS):
            if not DIO.value :
                bit_mask = 1 << bit_index
                GPIB_Char = GPIB_Char| bit_mask

    # start interpreting characters. We add int(0b0100000) to the py_address_integer bcause bits 6 and 7
    # serve as the listen and talk flags respectivly.
    #
    #Indenting the character interpretation blocks under the "if is_bus_instruction or listen:" case 
    #has produced unexpected problems. This merrits future examination.
    #########
    if GPIB_Char==int(0b0100000)+py_address_integer and not listen : #and is_bus_instruction is implied since we wouldn't have a valid address in that case
        #speed is essential for this case as the presence of a GPIB instrument appears to be communicated by a handshake with minimal delay time
        RFD.value=False #RFD is literal
        DAC.value=True #handshake DAC is literal.
        while not DAV.value: #handshake, DAV asserts low
        	pass #handshake
    	DAC.value=False #handshake DAC is literal.
        RFD.value=True #handshake

        listen=True
        if is_bus_instruction:
            pixels.fill((20, 0, 0))#The neopixel may be slow, so command it after the DAC and RFD commands.
            #This will set the Neo Pixel to solid red when MRE is asserted. The red-strobe loop above is bypassed
            #when MRE is asserted leaving the pixel solid red on.

    elif GPIB_Char==int(0b0111111):#clear all listeners or queries, depending on MRE flag.
        RFD.value=False  #handshake, RFD is literal.
        DAC.value=True  #handshake, DAC is literal.
        while not DAV.value: #handshake, DAV asserts low
        	pass #handshake
    	DAC.value=False #handshake DAC is literal.
        RFD.value=True #handshake

        if is_bus_instruction:
            #print("clear listeners")
            listen=False
            message_in_stack=""
        elif listen:
            #print("query recieved")
            message_in_stack=""
            query_flag=True

    elif listen: # If listen is true and the above cases were not entered, the DIO lines must contain a character forming part of an instruction.
        RFD.value=False #Handshake RFD is literal
        DAC.value=True #handshake DAC is literal.
        while not DAV.value: #handshake, DAV asserts low
        	pass #handshake
    	DAC.value=False #handshake DAC is literal.
        RFD.value=True # Handshake

        if GPIB_Char>64 and GPIB_Char<91: # This block converts uppercase to lowercase, for case insensitivity
            message_in_stack += chr(GPIB_Char+int(0b0100000))
        else:
            message_in_stack += chr(GPIB_Char)

        if message_in_stack=="*idn":
            string_out="Adafruit w code by Spidell,RP2040(KB2040 ver),01,type help? for commands" 
            #Format is OEM,Model,Serial,Firmware Version or equivalent. Commas are used as field delineators and semicolons are excluded.
            #The overall length of *idn response is limited to 70 characters per IEEE488.2 but 100 characters works with NI Max
            #empty fields should contain ascii character '0'
            print("transmit name")
        elif message_in_stack=="snd":
            Volts_1=analog_in_1.value*0.000050354
            string_out="digital number = " + str(analog_in_1.value) + ",  voltage = " + str(Volts_1) +" V  (uncalibrated)"
            #string_out="reading goes here"
            print("transmit data")
        elif message_in_stack=="help":
            string_out="Valid commands are *IDN?, SND?, BLINK, HELP?. Commands are case insensitive. BLINK runs the led (Neo Pixel) on the KB2040 through a color sequence. SND? reads the analog voltage on pin 'A0' or pin 18, using CCW pin count. Do not exceed 3.4 V."
            print("transmit help info")
        elif message_in_stack=="blink":
            message_in_stack=""
            blink_light_flag=True
            print("tri-color strobe sequence")
        #consider adding some error handling to clear the message if one of the
        #recognised commands is padded with erronious characters
        #and another case to clear strings exceeding a certian length

        print(bin(GPIB_Char), " ", GPIB_Char , " ", chr(GPIB_Char), " stacking")
        print(message_in_stack)

    elif is_bus_instruction:
        # This case is encountered if it was a bus insruction meant for another instrument.
        RFD.value=False #handshake RFD is  literal.
        DAC.value=True ##handshake DAC is literal.
        while not DAV.value : # Handshake
        	pass# Handshake
        time.sleep(0.012) #This delay is an interim solution to force a timeout at the controller
        #during a scan for instruments. This avoids causing the controller to detect instruments
        #at addresses where no instrument is present. Section 17-6 of the IEEE488.2 standard 
        #is suppoosed to describe how the scan process works, but my attempts have not 
        #enabled the pseudocode indicated in that document to function.
        DAC.value=False# Handshake. Ideally, these DAC and RFD assignments would be simutanious.
        RFD.value=True# Handshake
    #End of the character interpretation blocks which should be indented once the problems introduced 
    #by indenting can be worked out.
    #########
    elif is_remote:
        DAC.value=True #Handshake. DAC is literal.
        while not DAV.value : # Handshake
        	pass# Handshake
        DAC.value=False# Handshake. Ideally, these DAC and RFD assignments would be simutanious.
        RFD.value=True# Handshake
    else: #see if this can be commented out as this case might not be encountered in practice
        DAC.value=True#False# Handshake. Ideally, these DAC and RFD assignments would be simutanious.
        RFD.value=True# Handshake

    if query_flag and GPIB_Char==int(0b1000000)+py_address_integer:
        # Ok, now we can perform actions based on commands
        pixels.fill((0, 0, 128))
        #This will set the light bright blue while the transmission is ongoing
        #If the light stays blue, the process was interrupted, perhaps the controller faulted.
        #A future revision would be to implement a method for an operator to return the Raspberry Pi back to it's idle state.
        #Could be as simple as checking a DIO-connected pushbutton state, using that as a 'local' or 'reset'. 
        #Also, might need a timeout?
        DAC.direction = digitalio.Direction.INPUT
        DAC.pull = digitalio.Pull.UP
        RFD.direction = digitalio.Direction.INPUT
        RFD.pull = digitalio.Pull.UP
        DAV.switch_to_output(value=True, drive_mode=digitalio.DriveMode.OPEN_DRAIN) #very important that this changes to an output defaulted to 3.3 V
        MRE.switch_to_output(value=False, drive_mode=digitalio.DriveMode.OPEN_DRAIN)
        EOI.switch_to_output(value=True, drive_mode=digitalio.DriveMode.OPEN_DRAIN)
        for DIO in DIO_BUS:
            DIO.switch_to_output(value=True, drive_mode=digitalio.DriveMode.OPEN_DRAIN)

        print("sending response")

        string_out_len=len(string_out)-1 #since loops start at zero we need to compensate here (for speed) vs recalculating every loop
        for i, char in enumerate(string_out):
            char_as_num=ord(char) #char_as_num is flagged as an integer-encoded str
            #char_as_bin=bin(char_as_num) #char_as_bin is flagged as an bin-encoded str
            #print(char_as_bin)
            for bit_index, DIO in enumerate(DIO_BUS):
               bit_to_DIO=char_as_num>>bit_index & 1
               DIO.value=not(bit_to_DIO)

            if i==string_out_len:
                MRE.value=False #!!!!!special case for last character!!!!!!  assert bus instruction so that the EOI will be recognised
                EOI.value=False #!!!!!!special case for last character!!!!!! assert end of instruction
            DAV.value=False #Assert data availability
            while not DAC.value: #DAC is literal. Pause until data accepted (NDAC asserted).
                pass
            DAV.value=True #un-assert data availability
            if i==string_out_len:
                EOI.value=True #special case for last character un-assert assert end of instruction
            else:
                while not RFD.value: #pause until ready for data (NRFD asserted)
                    pass
        time.sleep(0.001)
        query_flag=False
        listen=False
        string_out="Nul"

        DAC.switch_to_output(value=False, drive_mode=digitalio.DriveMode.OPEN_DRAIN) #default should be low or un-assert since DAC and RFD are literal
        RFD.switch_to_output(value=False, drive_mode=digitalio.DriveMode.OPEN_DRAIN) #default should be low or un-assert since DAC and RFD are literal
        DAV.direction = digitalio.Direction.INPUT
        DAV.pull = digitalio.Pull.UP
        MRE.direction = digitalio.Direction.INPUT
        MRE.pull = digitalio.Pull.UP
        EOI.direction = digitalio.Direction.INPUT
        EOI.pull = digitalio.Pull.UP
        for DIO in DIO_BUS:
            DIO.direction = digitalio.Direction.INPUT
            DIO.pull = digitalio.Pull.UP #testing this

        RFD.value=True

    #demonstrating a response to a simple 'write' (the above case is a query). Here we simply strobe the onboard NeoPxel
    elif blink_light_flag :
        r=0
        g=0
        b=0
        while r < 150:
            r=r+1
            pixels.fill((r, 0, 0))
            time.sleep(0.005)
        while r >1 :
            r=r-1
            pixels.fill((r, 0, 0))
            time.sleep(0.005)
        while g < 150:
            g=g+1
            pixels.fill((0, g, 0))
            time.sleep(0.005)
        while g >1 :
            g=g-1
            pixels.fill((0, g, 0))
            time.sleep(0.005)
        while b < 150:
            b=b+1
            pixels.fill((0, 0, b))
            time.sleep(0.005)
        while b >1 :
            b=b-1
            pixels.fill((0, 0, b))
            time.sleep(0.005)
        blink_light_flag=False

print("End of program. ")
